package mapspinner.util;

import java.net.URISyntaxException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import mapspinner.Terrain;
import mapspinner.TerrainKey;
import mapspinner.TerrainNode;

import com.jme.image.Image;
import com.jme.math.FastMath;
import com.jme.math.Vector3f;
import com.jme.system.DisplaySystem;
import com.jme.util.TextureManager;
import com.jme.util.resource.MultiFormatResourceLocator;
import com.jme.util.resource.ResourceLocatorTool;

public class TerrainEditor {
	static {
		try {
			final URL baseDir = TerrainEditor.class.getClassLoader()
					.getResource("textures/brushes/");
			final MultiFormatResourceLocator locator = new MultiFormatResourceLocator(
					baseDir, new String[] { ".tga", ".bmp", ".png", ".jpg",
							".texture", ".jme" });
			ResourceLocatorTool.addResourceLocator(
					ResourceLocatorTool.TYPE_TEXTURE, locator);
		} catch (final URISyntaxException e) {
			e.printStackTrace();
		}
	}

	private final Terrain mapSpinner;

	private final int PAINT = 0;

	private final int SCULPT = 1;
	private final int SCULPT_AVERAGE = 2;

	private final int SCULPT_SMOOTH = 3;
	private static final int X = 0;

	private static final int Z = 1;
	private static final int SIZE = 2;
	private final int[] mapInfo;
	ArrayList<float[]> brushes = new ArrayList<float[]>();

	float[] points;

	public TerrainEditor(final Terrain mapSpinner) {
		this.mapSpinner = mapSpinner;
		mapInfo = new int[] { 0, 0,
				mapSpinner.getSettings().getResolution() - 1 };
	}

	public void average(final Vector3f startingPoint, final int brushNum,
			final float brushScale, final float strength) {
		stroke(startingPoint, brushNum, brushScale, strength, null, 0,
				SCULPT_AVERAGE);
	}

	private float[] collectPoints(final int[] brushInfo,
			final Map<TerrainKey, TerrainNode> terrainsToUpdate,
			final int action) {
		if (action != PAINT)
			if (points == null
					|| points.length != brushInfo[SIZE] * brushInfo[SIZE])
				points = new float[brushInfo[SIZE] * brushInfo[SIZE]];
		for (int x = 0; x < brushInfo[SIZE]; ++x)
			for (int z = 0; z < brushInfo[SIZE]; ++z) {
				final int xWorldIndex = brushInfo[X] + x;
				final int zWorldIndex = brushInfo[Z] + z;
				// get the index to modify on the map
				mapInfo[X] = xWorldIndex - indexToKey(xWorldIndex)
						* mapInfo[SIZE];
				mapInfo[Z] = zWorldIndex - indexToKey(zWorldIndex)
						* mapInfo[SIZE];

				// this is the currently edited mapkey
				final TerrainKey key = new TerrainKey(indexToKey(xWorldIndex),
						indexToKey(zWorldIndex));
				final TerrainNode terrain = putMap(key, terrainsToUpdate);
				if (terrain == null)
					return null;
				if (mapInfo[X] == 0 && mapInfo[Z] == 0) {
					if (putMap(new TerrainKey((short) (key.x - 1),
							(short) (key.z - 1)), terrainsToUpdate) == null)
						return null;
				} else {
					if (mapInfo[X] == 0)
						if (putMap(new TerrainKey((short) (key.x - 1), key.z),
								terrainsToUpdate) == null)
							return null;
					if (mapInfo[Z] == 0)
						if (putMap(new TerrainKey(key.x, (short) (key.z - 1)),
								terrainsToUpdate) == null)
							return null;
				}
				if (action != PAINT)
					points[x + z * brushInfo[SIZE]] = terrain.getPoint(
							mapInfo[X], mapInfo[Z]);
			}
		return points;
	}

	private float getBrushPoint(float x, float z, final float[] brush) {
		final int size = (int) FastMath.sqrt(brush.length);
		x = x * size;
		z = z * size;
		final float col = FastMath.floor(x);
		final float row = FastMath.floor(z);

		if (col < 0 || row < 0 || col >= size - 1 || row >= size - 1)
			return 0;

		final float intOnX = x - col, intOnZ = z - row;
		float topLeft, topRight, bottomLeft, bottomRight;
		final int focalSpot = (int) (col + row * size);

		topLeft = brush[focalSpot];
		topRight = brush[focalSpot + 1];
		bottomLeft = brush[focalSpot + size];
		bottomRight = brush[focalSpot + size + 1];

		// Use linear interpolation to find the return value.
		if (intOnX > intOnZ)
			return (1 - intOnX) * topLeft + (intOnX - intOnZ) * topRight
					+ intOnZ * bottomRight;
		else
			return (1 - intOnZ) * topLeft + (intOnZ - intOnX) * bottomLeft
					+ intOnX * bottomRight;
	}

	private short indexToKey(final float input) {
		return (short) FastMath.floor(input / mapInfo[SIZE]);
	}

	public void loadBrush(final String fileName) {
		final Image image = TextureManager.loadImage(fileName, false);
		final ByteBuffer bb = image.getData(0);
		final float[] brush = new float[bb.limit() / 3];
		for (int x = 0; x < brush.length; x++) {
			final byte[] color = new byte[3];
			color[0] = bb.get();
			color[1] = bb.get();
			color[2] = bb.get();
			// this scale is based on 1/3/256 (average of three values of up to
			// 256
			brush[x] = 0.00130208f * (ubyte(color[0]) + ubyte(color[1]) + ubyte(color[2]));
		}
		brushes.add(brush);
	}

	public void paint(final Vector3f startingPoint, final int brushNum,
			final float brushScale, final float strength,
			final String fileName, final int layer) {
		stroke(startingPoint, brushNum, brushScale, strength, fileName, layer,
				PAINT);
	}

	private TerrainNode putMap(final TerrainKey key,
			final Map<TerrainKey, TerrainNode> terrainsToUpdate) {
		TerrainNode terrain = terrainsToUpdate.get(key);
		if (terrain == null)
			terrain = mapSpinner.get(key);
		terrainsToUpdate.put(key, terrain);
		return terrain;
	}

	public void sculpt(final Vector3f startingPoint, final int brushNum,
			final float brushScale, final float strength) {
		stroke(startingPoint, brushNum, brushScale, strength, null, 0, SCULPT);
	}

	private void setPoint(final float[] points, final int px, final int pz,
			final int psize, final int x, final int y,
			final Vector3f brushOffset, final float[] brush,
			final float strength, final TerrainNode terrain,
			final String texture, final int layer, final int action) {
		final float brushPoint = getBrushPoint(brushOffset.x, brushOffset.z,
				brush)
				* strength / mapSpinner.getSettings().getResolution();
		if (action == SCULPT) {
			final float point = points[px + pz * psize] + brushPoint;
			terrain.setPoint(x, y, point);
		} else if (action == PAINT)
			terrain.getTerrainTexture().paint(x, y, brushPoint, texture, layer);
		else if (action == SCULPT_AVERAGE) {
			float average = 0;
			for (final float point : points)
				average += point;
			average /= points.length;
			average = FastMath.LERP(brushPoint * strength, points[px + pz
					* psize], average);
			terrain.setPoint(x, y, average);
		} else if (action == SCULPT_SMOOTH) {
			int pointsAround = 0;
			float topLeft = 0, topRight = 0, bottomLeft = 0, bottomRight = 0;
			if (px > 0 && pz > 0) {
				topLeft = points[px - 1 + (pz - 1) * psize];
				++pointsAround;
			}
			if (px < psize - 2 && pz > 0) {
				topRight = points[px + 1 + (pz - 1) * psize];
				++pointsAround;
			}
			if (px > 0 && pz < psize - 2) {
				bottomLeft = points[px - 1 + (pz + 1) * psize];
				++pointsAround;
			}
			if (px < psize - 2 && pz < psize - 2) {
				bottomRight = points[px + 1 + (pz + 1) * psize];
				++pointsAround;
			}
			float average = (topLeft + topRight + bottomLeft + bottomRight)
					/ pointsAround;
			average = FastMath.LERP(brushPoint * strength, points[px + pz
					* psize], average);
			terrain.setPoint(x, y, average);
		}
	}

	public void smooth(final Vector3f startingPoint, final int brushNum,
			final float brushScale, final float strength) {
		stroke(startingPoint, brushNum, brushScale, strength, null, 0,
				SCULPT_SMOOTH);
	}

	private void stroke(final Vector3f startingPoint, final int brushNum,
			final float brushScale, final float strength, final String texture,
			final int layer, final int action) {
		// move the starting point out half the brushes size so we paint in the
		// middle
		startingPoint.x -= brushScale * 0.5;
		startingPoint.z -= brushScale * 0.5;

		final int[] brushInfo = new int[3];
		brushInfo[X] = toWorldIndex(startingPoint.x);
		brushInfo[Z] = toWorldIndex(startingPoint.z);
		brushInfo[SIZE] = toWorldIndex(startingPoint.x + brushScale)
				- brushInfo[X];

		final HashMap<TerrainKey, TerrainNode> mapsToUpdate = new HashMap<TerrainKey, TerrainNode>();
		final float[] points = collectPoints(brushInfo, mapsToUpdate, action);
		writePoints(points, brushInfo, mapsToUpdate, brushes.get(brushNum),
				strength, texture, layer, action);

		for (final TerrainNode terrain : mapsToUpdate.values()) {
			terrain.edited = true;
			final TerrainNode[] maps = mapSpinner.nodesAround(terrain.getKey());
			if (action != PAINT) {
				terrain.calcNormals();
				terrain.stitchNormals(maps);
				terrain.getTerrainTexture().autoTexture();

				terrain.setState((byte) (TerrainNode.VBO_NORMALS_APPLIED - 1));
			} else
				terrain.setState((byte) (TerrainNode.VBO_COLOR_APPLIED - 1));

			while (terrain.getState() < TerrainNode.VBO_COLOR_APPLIED)
				terrain.update(DisplaySystem.getDisplaySystem().getRenderer());
		}
	}

	private int toWorldIndex(final float input) {
		return (int) (input * 1 / mapSpinner.getSettings().getHorizontalScale());
	}

	public int ubyte(final int input) {
		if (input < 0)
			return input + 256;
		return input;
	}

	private void writePoints(final float[] points, final int[] brushInfo,
			final Map<TerrainKey, TerrainNode> terrainsToUpdate,
			final float[] brush, final float strength, final String texture,
			final int layer, final int action) {
		// look up and save the height from the brush
		// final int mapSize = mapInfo[SIZE] - 1;
		for (int x = 0; x < brushInfo[SIZE]; ++x)
			for (int z = 0; z < brushInfo[SIZE]; ++z) {
				final int xWorldIndex = brushInfo[X] + x;
				final int zWorldIndex = brushInfo[Z] + z;
				// get the index to modify on the map
				mapInfo[X] = xWorldIndex - indexToKey(xWorldIndex)
						* mapInfo[SIZE];
				mapInfo[Z] = zWorldIndex - indexToKey(zWorldIndex)
						* mapInfo[SIZE];

				// this is the currently edited mapkey
				final TerrainKey key = new TerrainKey(indexToKey(xWorldIndex),
						indexToKey(zWorldIndex));
				TerrainNode terrain = terrainsToUpdate.get(key);

				// get the uv coordinates for the brush lookup
				final Vector3f brushOffset = new Vector3f(1f / brushInfo[SIZE]
						* x, 0, 1f / brushInfo[SIZE] * z);

				setPoint(points, x, z, brushInfo[SIZE], mapInfo[X], mapInfo[Z],
						brushOffset, brush, strength, terrain, texture, layer,
						action);

				// Internal stitching

				if (mapInfo[X] == 0 && mapInfo[Z] == 0) {
					terrain = mapSpinner.get(new TerrainKey(
							(short) (key.x - 1), (short) (key.z - 1)));
					setPoint(points, x, z, brushInfo[SIZE], mapInfo[SIZE],
							mapInfo[SIZE], brushOffset, brush, strength,
							terrain, texture, layer, action);
					terrain = mapSpinner.get(new TerrainKey(
							(short) (key.x - 1), key.z));
					setPoint(points, x, z, brushInfo[SIZE], mapInfo[SIZE], 0,
							brushOffset, brush, strength, terrain, texture,
							layer, action);
					terrain = mapSpinner.get(new TerrainKey(key.x,
							(short) (key.z - 1)));
					setPoint(points, x, z, brushInfo[SIZE], 0, mapInfo[SIZE],
							brushOffset, brush, strength, terrain, texture,
							layer, action);
				} else {
					if (mapInfo[X] == 0) {
						terrain = mapSpinner.get(new TerrainKey(
								(short) (key.x - 1), key.z));
						setPoint(points, x, z, brushInfo[SIZE], mapInfo[SIZE],
								mapInfo[Z], brushOffset, brush, strength,
								terrain, texture, layer, action);
					}
					if (mapInfo[Z] == 0) {
						terrain = mapSpinner.get(new TerrainKey(key.x,
								(short) (key.z - 1)));
						setPoint(points, x, z, brushInfo[SIZE], mapInfo[X],
								mapInfo[SIZE], brushOffset, brush, strength,
								terrain, texture, layer, action);
					}
				}

			}
	}
}
